#!/usr/bin/env python

import SocketServer, re, socket, sys, pickle, hashlib, os, traceback, time, random, copy
import subprocess
from lib import *
from optparse import OptionParser

class ClientError(Exception):
    "An exception thrown because the client gave bad input to the server."
    pass

class NonExistantCharacter(Exception):
  "An exception thrown because the client tried to login as a nonexistant character."
  pass

class AuthenticationError(Exception):
  "An exception thrown because the client entered an incorrect password."
  pass

class NewCharacter(Exception):
  "An exception where the user wants to create a new character."
  pass

class MudServer(SocketServer.ThreadingTCPServer):
    "The server. It's really no more complicated or simple than that."
    allow_reuse_address = True
    
    def __init__(self, server_address, RequestHandlerClass):
        SocketServer.ThreadingTCPServer.__init__(self, server_address, RequestHandlerClass)
        random.seed(time.time())
        self.users = {}
        self.connections = {}
        self.up = True
        self.rebooting = False
        self.permTable = self.loadPerms() # This must happen before buildCommandTable()
        self.roles = self.loadRoles()
        self.commandTable = self.buildCommandTable()
        self.interpreter = Interpreter.Interpreter(self.commandTable,self.permTable)
        self.mudname = "Quest-based Adventuring"
        self.initTime = time.time()
        self.timers = {}
        self.combats = []
        # Oh man, zones...
        self.areas     = []
        self.instances = []
        # Load stuff
        self.loadAreas()

    
    def gameLoop(self):
      """
      This is the big fat game loop. 
      It is called every 10th of a second
      """
      self.handle_request() # Handle new socket connections
      self.autoSave()
      self.combatLoop()
      self.regenLoop()
      self.effectsLoop() # lol

        
    def autoSave(self):
      "This saves every character that is logged on."
      t = time.time()
      if "lastAutoSave" not in self.timers:
        self.timers["lastAutoSave"] = self.initTime
      if "tickLength" not in self.timers:
        self.timers["tickLength"] = random.randint(30,60)
      if t >= self.timers["lastAutoSave"] + self.timers["tickLength"]:
        old = self.timers["tickLength"]
        self.timers["tickLength"] = random.randint(30,60)
        self.timers["lastAutoSave"] = t
        # Save every character
        for c in self.connections.itervalues():
          c.save()


    def combatLoop(self):
      "Used to drive combat"
      try:
        for (c,v,f) in self.combats:
          f()
      except Exception, e:
        print e


    def regenLoop(self):
      "Regenerate health and mana every 5 seconds"
      t = time.time()
      if "regen" not in self.timers:
        self.timers["regen"] = t
      try:
        if t >= self.timers["regen"]+5:
          self.timers["regen"] = t
          for c in self.connections.itervalues():
            player = c.character
            if not player.combat:
              hpregen = int(player.resilience*.05)
              mpregen = int(player.sagacity*.05)
              player.regen(hpregen,mpregen)
            else:
              hpregen = 0
              mpregen = int(player.sagacity*.01)
              player.regen(hpregen,mpregen)
      except:
        pass
    
    
    def effectsLoop(self):
      "Make sure all effects are applied properly every second"
      t = time.time()
      if "effects" not in self.timers:
        self.timers["effects"] = t-1
      try:
        if t >= self.timers["effects"]+1:
          self.timers["effects"] = t
          for c in self.connections.itervalues():
            c.character.applyBonuses()
      except:
        pass
    
    
    def loadRoles(self):
      try:
        print "Loading roles..."
        r = open("settings/roles","r")
        roles = pickle.load(r)
        r.close()
        return roles
      except:
        return []
    
    
    def saveRoles(self):
      print "Saving roles..."
      r = open("settings/roles", "w")
      pickle.dump(self.roles, r)
      r.close()
      return
    
    
    def loadPerms(self):
      try:
        print "Loading Permissions..."
        permfile = open("settings/permissions", "r")
        permTable = pickle.load(permfile)
        permfile.close()
        return permTable
      except:
        return {}
        
        
    def savePerms(self):
      print "Saving permissions..."
      p = open("settings/permissions", "w")
      pickle.dump(self.permTable, p)
      p.close()
      return
    
    
    def loadAreas(self):
      for f in os.listdir("areas"):
        if Area.AREA_FILENAME_REGEXP.match(f):
          try:
            print "Loading %s..." % f
            area = open("areas/%s" % f, "r")
            self.areas.append(pickle.load(area))
            area.close()
            self.areas[-1].server = self
            self.areas[-1].instantiate()
          except:
            print "Couldn't load %s..." % f
            print sys.exc_info()[0]
            print sys.exc_info()[1]
            print traceback.extract_tb(sys.exc_info()[2])
              
              
    def buildCommandTable(self):
      """
      Scans all loaded modules from the lib directory for %s_command named functions and registers them for use.
      Also applies permission restrictions to these commands in the command table as per /settings/permissions
      """
      import lib
      cmdregexp = re.compile('^(\w+)_command$')
      table = {}
      for mudModule in lib.__all__:
        mudModule = eval(mudModule)
        members = dir(mudModule)
        # TODO
        # Need to make this open up classes too
        for m in members:
          func = cmdregexp.match(m)
          if func is not None:
            ref = getattr(mudModule,m)
            table[func.groups()[0]] = ref
      return table
    
    
    def verify_request(self, request, client_address):
        return True
    
    
    def shutdown(self):
      "Shutdown the server"
      for u in self.users.keys():
        del self.users[u]
      for c in self.connections.values():
        try:
          c.save()
          c.request.shutdown(2)
          c.request.close()
        except:
          print sys.exc_info()[0]
          print sys.exc_info()[1]
          print traceback.extract_tb(sys.exc_info()[2])
          print "%s had problems!" % c        
      self.socket.shutdown(socket.SHUT_RDWR)
      self.socket.close()
      self.server_close()
      self.up = False


    def reboot(self):
      "Reboot the server"
      self.rebooting = True
      self.shutdown()
    
    
    def copyover(self):
      "Perform a copyover"
      copyoverStruct = {}
      for c in self.connections.values():
        c.save()
        copyoverStruct[c.name] = (c.client_address,c.request.fileno())
      print copyoverStruct
      


class RequestHandler(SocketServer.StreamRequestHandler):
    NAME = re.compile("^[A-Za-z0-9_-]+$")
    
    def handle(self):
        welcomebuf = "${blue,bold}Welcome to %s!${reset}\n" % (self.server.mudname)
        self.name = self.promptName(welcomebuf + "Please enter your name or type ${white,bold}new${reset} to create a character:")
        self.done = False
        self.replacing = False
        
        self.handleLogin()
        
        while not self.done:
            try:
                self.processInput()
            except ClientError, error:
                self.echo(str(error))
            except socket.error:
                self.done = True
            except:
              print "socket loop received a weird exception"
              print sys.exc_info()[0]
              print sys.exc_info()[1]
              print traceback.extract_tb(sys.exc_info()[2])
    
        
    def finish(self):
        try:
          if self.character and self.character.name:
              message = "%s has quit." % (self.character.name)
            
              self.worldEcho(message, False)
             
              if self.replacing is not True:          
                if self.server.users.get(self.character.name):
                  del(self.server.users[self.character.name])
                if self.server.connections.get(self.character.name):
                  del(self.server.connections[self.character.name])
        except:
          pass
          
        try:                    
          self.request.shutdown(2)
          self.request.close()       
        except:
          pass
    
    
    def handleLogin(self):
      "Handle initial logging in of a player"
      try:
        self.loadChar(self.name)
      except ClientError, error:
        print error.args[0]
        self.done = True
      except NonExistantCharacter, error:
        self.echo(error.args[0],False)
        self.createChar(self.name,False)
      except NewCharacter:
        self.createChar(self.promptName("Create a new character with what name?:"))
      except AuthenticationError, error:
        self.echo(error.args[0])
        self.done = True
      except socket.error:
        self.done = True
      
    
    def promptName(self, promptStr):
      self.echo(promptStr)
      return self._readline().capitalize()
    

    def createChar(self, name, new=True):
      if not name:
        print "createChar: Did not receive a name!"
        self.done = True
        return
        
      if self.charExists(name):
        self.createChar(self.promptName("There already exists a character named %s, please choose another:" % name))
        return
      
      self.echo("Would you like to create a character named %s? (${white,bold}type yes or no${reset})" % name )
      response = self._readline().lower()
      if response == "yes":
        pass
      elif response == "no":
        if new is True:
          self.createChar(self.promptName("Ok. What name would you like instead?:"))
        else:
          self.name = self.promptName("Ok, please enter your name or type ${white,bold}new${reset} to create a character:")
          self.handleLogin()
        return
      else:
        self.done = True
        return
        
      self.echo("Please choose a password for %s." % name)
      password = self._readline()
      if password is None or len(password) < 6:
        self.echo("Your password must be at least 6 characters long.")
        self.createChar(name)
        return
      elif len(password) > 5:
        self.character = Player.Character({
                          "password": hashlib.sha224(password).hexdigest(),
                          "name": name.capitalize(),
                          "title": "the nameless",
                          "loginTime": time.time(),
                          "d":self})
        pfile = open("characters/%s.character" % name.lower(), "w")
        pickle.dump(self.character,pfile)
        pfile.close()
        self.login(self.character.name)
        return
      
      print "Not sure how we got here..."
      print "name => %s, password => %s" % (name,password)
      print sys.exc_info()[0]
      print sys.exc_info()[1]
      print traceback.extract_tb(sys.exc_info()[2])
      
      self.done = True
      return


    def charExists(self, name):
      return os.path.exists("characters/%s.character" % name.lower())

    def loadChar(self, name):
        if not name:
          raise ClientError, "No name provided."
        if not self.NAME.match(name):
          raise ClientError, "Invalid name provided." 
        if name.lower() == "new":
          raise NewCharacter
        if not self.charExists(name): 
          raise NonExistantCharacter, "That character does not exist."
        else:
          self.echo("Please enter your password:")
          password = self._readline()
          pfile = open("characters/%s.character" % name.lower(), "r")
          chf = pickle.load(pfile)
          pfile.close()
          
          if chf.password != hashlib.sha224(password).hexdigest():
            raise AuthenticationError, "Incorrect Password."
          
        self.login(name,chf)
    
    
    def login(self,name,characterFile=None):
      # kick the already logged in guy...
      if name in self.server.connections:
        print "%s is being replaced..." % name
        replace = self.server.connections[name]
        replace.save()
        replace.done = True
        replace.character.toSelf("Someone else has logged in as %s. This connection will be replaced." % name)
        replace.replacing = True

      if characterFile is not None:
        self.character = Player.Character(characterFile)
        self.character.d = self
        self.character.playedLastChecked = 0
        self.character.loginTime = time.time()
      
      self.server.connections[name] = self
      self.server.users[name] = self.wfile
      self.echo("Hello %s." % self.character.name)
      self.worldEcho("${magenta,bold}%s has joined." % name, False)
    
    
    def save(self):
      pfile = open("characters/%s.character" % self.character.name.lower(), "w")
      pickle.dump(self.character,pfile)
      pfile.close()
    
    
    def worldEcho(self, message, includeThisUser = True):
        message = self._ensureNewLine(Color.colorize(message))
        for user, output in self.server.users.items():
            if includeThisUser or user != self.character.name:
                output.write(message)
                output.write(self.server.connections[user].prompt())
    
    
    def prompt(self):
      try:
        hp = self.character.health
        mp = self.character.mana
        return Color.colorize("\n${red,bold}%s${black,bold}|${blue,bold}%s${black,bold}>${reset} " % (hp,mp) )
      except:
        return Color.colorize("\n${black,bold}> ")
    
    
    def echo(self, message, prompt=True):
        message = Color.colorize(message)
        self.wfile.write( "%s\n" % message.strip() )
        if prompt:
          self.wfile.write( self.prompt() )
    
    
    def _readline(self):
        return self.rfile.readline(512).strip()
    
    
    def _ensureNewLine(self, s):
        if s:
          return "\n%s" % s.strip()
        else:
          return "\n"
    
    
    def processInput(self):
      message = self._readline()
      # Interpreter Testing
      try:
        self.server.interpreter.interpret(self.character,message)
      except Interpreter.CommandNotFound:
        if len(message) > 0:
          self.echo("Huh?")
        else:
          self.wfile.write(self.prompt())
      except:
        print sys.exc_info()[0]
        print sys.exc_info()[1]
        print traceback.extract_tb(sys.exc_info()[2])


if __name__ == "__main__":
  parser = OptionParser()
  parser.add_option("-p", "--port", dest="port",
                  help="run the server on port", metavar="PORT")
  (options, args) = parser.parse_args()
  
  if options.port is not None:
    port = int(options.port)
  else:
    port = 4000
  
  mudServer = MudServer(('0.0.0.0', port), RequestHandler)
  mudServer.socket.setblocking(0) # trying...
  try:
    while mudServer.up:
      mudServer.gameLoop()
      time.sleep(0.1) # pure fucking magic
    mudServer.server_close()
  finally:
    mudServer.server_close()
    if mudServer.rebooting is True:
      print "Attempting to reboot..."
      pid = subprocess.Popen(["python", "server.py", str(port)]).pid
      print pid

